package com.xzy.mybatis.cipher.plugin;

import cn.hutool.core.util.StrUtil;
import com.xzy.mybatis.cipher.spring.MybatisCipherContext;
import com.xzy.mybatis.cipher.support.DatasourceHolder;
import com.xzy.mybatis.cipher.support.strategy.crypto.CryptoStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;

/**
 * Mybatis 插件：解密
 *
 * @author xzy.xiao
 * @since 2025/1/15  15:03
 */
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class DecryptPlugin implements Interceptor {

    private final DatasourceHolder datasourceHolder;
    private final MybatisCipherContext mybatisCipherContext;

    public DecryptPlugin(DatasourceHolder datasourceHolder, MybatisCipherContext mybatisCipherContext) {
        this.datasourceHolder = datasourceHolder;
        this.mybatisCipherContext = mybatisCipherContext;
    }

    /**
     * Intercepts the execution of the original method, first executes the original logic, then decrypts the returned result.
     *
     * @param invocation Invocation information, containing the method to execute and its parameters
     * @return The result of the original method execution
     * @throws Throwable Throws an exception if the original method execution fails
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("current datasource name = {}", datasourceHolder.getCurrentDatasourceName());

        // Continue the execution of the original method
        Object resultObject = invocation.proceed();

        // Decrypt and replace result
        decrypt(resultObject);

        return resultObject;
    }

    private void decrypt(Object resultObject) throws IllegalAccessException {
        if (null == resultObject) {
            return;
        }

        if (resultObject instanceof ArrayList) {
            // select list
            doDecrypt((ArrayList) resultObject);
        } else {
            // select one
            doDecrypt(resultObject);
        }
    }

    private <T> void doDecrypt(ArrayList<T> resultList) throws IllegalAccessException {
        if (CollectionUtils.isEmpty(resultList)) {
            return;
        }

        for (Object result : resultList) {
            doDecrypt(result);
        }
    }

    public <T> void doDecrypt(T result) throws IllegalAccessException {
        if (null == result) {
            return;
        }

        Class<?> resultClass = result.getClass();
        if (!mybatisCipherContext.getCryptoMetadataRegistry().isCryptoClass(resultClass.getName())) {
            return;
        }

        log.debug("Decrypt result.\n\t==> original property value:{}", result);

        Field[] fieldList = resultClass.getDeclaredFields();
        for (Field field : fieldList) {

            if (!mybatisCipherContext.getCryptoMetadataRegistry().isCryptoField(resultClass, field)) {
                continue;
            }

            CryptoStrategy cryptoStrategy = mybatisCipherContext.getDataCryptoStrategyRegistry().getStrategy(resultClass, field);
            if (cryptoStrategy == null) {
                continue;
            }

            ReflectionUtils.makeAccessible(field);
            String encryptedValue = (String) ReflectionUtils.getField(field, result);
            if (StrUtil.isBlank(encryptedValue)) {
                continue;
            }

            String decryptedValue = cryptoStrategy.decrypt(encryptedValue);
            field.set(result, decryptedValue);
        }

        log.debug("Decrypt select result.\n\t==> decrypted property value:{}", result);
    }
}
